﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

namespace Mp3Observer.Common.Messaging
{
    /// <summary>
    /// A simple message class, carrying optional information about the message's sender.
    /// </summary>
    public class Messenger : IMessenger
    {
        /// <summary>
        /// the default Messenger instance.
        /// </summary>
        private static Messenger defaultInstance;

        /// <summary>
        /// list of recipients that will received also derived messages.
        /// </summary>
        private Dictionary<Type, List<WeakAction>> recipientsOfSubclassesAction;

        /// <summary>
        /// list of recipients
        /// </summary>
        private Dictionary<Type, List<WeakAction>> recipientsStrictAction;

        /// <summary>
        /// Gets the Messenger's default instance, allowing
        /// to register and broadcast in a static manner.
        /// </summary>
        public static Messenger Default
        {
            get
            {
                if (defaultInstance == null)
                    defaultInstance = new Messenger();
                return defaultInstance;
            }
        }

        /// <summary>
        /// Sets the Messenger's default (static) instance to null.
        /// </summary>
        public static void Reset()
        {
            defaultInstance = null;
        }

        /// <summary>
        /// Registers a recipient for a type of message TMessage. The action
        /// parameter will be executed when a corresponding message is sent.
        /// <para>Registering a recipient does not create a hard reference to it,
        /// so if this recipient is deleted, no memory leak is caused.</para>
        /// </summary>
        /// <typeparam name="TMessage">The type of message that the recipient registers
        /// for.</typeparam>
        /// <param name="recipient">The recipient that will receive the messages.</param>
        /// <param name="action">The action that will be executed when a message
        /// of type TMessage is sent.</param>
        public void Register<TMessage>(object recipient, Action<TMessage> action)
        {
            this.Register(recipient, false, action);
        }

        /// <summary>
        /// Registers a recipient for a type of message TMessage.
        /// The action parameter will be executed when a corresponding 
        /// message is sent. See the receiveDerivedMessagesToo parameter
        /// for details on how messages deriving from TMessage (or, if TMessage is an interface,
        /// messages implementing TMessage) can be received too.
        /// <para>Registering a recipient does not create a hard reference to it,
        /// so if this recipient is deleted, no memory leak is caused.</para>
        /// </summary>
        /// <typeparam name="TMessage">The type of message that the recipient registers
        /// for.</typeparam>
        /// <param name="recipient">The recipient that will receive the messages.</param>
        /// <param name="receiveDerivedMessagesToo">If true, message types deriving from
        /// TMessage will also be transmitted to the recipient. For example, if a SendOrderMessage
        /// and an ExecuteOrderMessage derive from OrderMessage, registering for OrderMessage
        /// and setting receiveDerivedMessagesToo to true will send SendOrderMessage
        /// and ExecuteOrderMessage to the recipient that registered.
        /// <para>Also, if TMessage is an interface, message types implementing TMessage will also be
        /// transmitted to the recipient. For example, if a SendOrderMessage
        /// and an ExecuteOrderMessage implement IOrderMessage, registering for IOrderMessage
        /// and setting receiveDerivedMessagesToo to true will send SendOrderMessage
        /// and ExecuteOrderMessage to the recipient that registered.</para>
        /// </param>
        /// <param name="action">The action that will be executed when a message
        /// of type TMessage is sent.</param>
        public void Register<TMessage>(object recipient, bool receiveDerivedMessagesToo, Action<TMessage> action)
        {
            var messageType = typeof(TMessage);
            Dictionary<Type, List<WeakAction>> recipients;

            if (receiveDerivedMessagesToo)
            {
                if (this.recipientsOfSubclassesAction == null)
                    this.recipientsOfSubclassesAction = new Dictionary<Type, List<WeakAction>>();
                recipients = this.recipientsOfSubclassesAction;
            }
            else
            {
                if (this.recipientsStrictAction == null)
                    this.recipientsStrictAction = new Dictionary<Type, List<WeakAction>>();
                recipients = this.recipientsStrictAction;
            }

            List<WeakAction> list;
            if (!recipients.ContainsKey(messageType))
            {
                list = new List<WeakAction>();
                recipients.Add(messageType, list);
            }
            else
                list = recipients[messageType];

            var weakAction = new WeakAction<TMessage>(recipient, action);
            list.Add(weakAction);

            this.Cleanup();
        }

        /// <summary>
        /// Sends a message to registered recipients. The message will
        /// reach all recipients that registered for this message type
        /// using one of the Register methods.
        /// </summary>
        /// <typeparam name="TMessage">The type of message that will be sent.</typeparam>
        /// <param name="message">The message to send to registered recipients.</param>
        public void Send<TMessage>(TMessage message)
        {
            this.SendToTargetOrType(message, null);
        }

        /// <summary>
        /// Sends a message to registered recipients. The message will
        /// reach only recipients that registered for this message type
        /// using one of the Register methods, and that are
        /// of the targetType.
        /// </summary>
        /// <typeparam name="TMessage">The type of message that will be sent.</typeparam>
        /// <typeparam name="TTarget">The type of recipients that will receive
        /// the message. The message won't be sent to recipients of another type.</typeparam>
        /// <param name="message">The message to send to registered recipients.</param>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "This syntax is more convenient than other alternatives.")]
        public void Send<TMessage, TTarget>(TMessage message)
        {
            this.SendToTargetOrType(message, typeof(TTarget));
        }

        /// <summary>
        /// Unregisters a message recipient for a given type of messages only. 
        /// After this method is executed, the recipient will not receive messages
        /// of type TMessage anymore, but will still receive other message types (if it
        /// registered for them previously).
        /// </summary>
        /// <typeparam name="TMessage">The type of messages that the recipient wants
        /// to unregister from.</typeparam>
        /// <param name="recipient">The recipient that must be unregistered.</param>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "The type parameter TMessage identifies the message type that the recipient wants to unregister for.")]
        public void Unregister<TMessage>(object recipient)
        {
            this.Unregister<TMessage>(recipient, null);
        }

        /// <summary>
        /// Unregisters a messager recipient completely. After this method
        /// is executed, the recipient will not receive any messages anymore.
        /// </summary>
        /// <param name="recipient">The recipient that must be unregistered.</param>
        public virtual void Unregister(object recipient)
        {
            UnregisterFromLists(recipient, this.recipientsOfSubclassesAction);
            UnregisterFromLists(recipient, this.recipientsStrictAction);
        }

        /// <summary>
        /// Unregisters a message recipient for a given type of messages and for
        /// a given action. Other message types will still be transmitted to the
        /// recipient (if it registered for them previously). Other actions that have
        /// been registered for the message type TMessage and for the given recipient (if
        /// available) will also remain available.
        /// </summary>
        /// <typeparam name="TMessage">The type of messages that the recipient wants
        /// to unregister from.</typeparam>
        /// <param name="recipient">The recipient that must be unregistered.</param>
        /// <param name="action">The action that must be unregistered for
        /// the recipient and for the message type TMessage.</param>
        public void Unregister<TMessage>(object recipient, Action<TMessage> action)
        {
            UnregisterFromLists(recipient, action, this.recipientsStrictAction);
            UnregisterFromLists(recipient, action, this.recipientsOfSubclassesAction);
            this.Cleanup();
        }

        /// <summary>
        /// Cleanups the list.
        /// </summary>
        /// <param name="lists">The lists.</param>
        private static void CleanupList(IDictionary<Type, List<WeakAction>> lists)
        {
            if (lists == null)
                return;

            var listsToRemove = new List<Type>();
            foreach (var list in lists)
            {
                var recipientsToRemove = new List<WeakAction>();
                foreach (var recipient in list.Value)
                    if (recipient == null || !recipient.IsAlive)
                        recipientsToRemove.Add(recipient);

                foreach (var recipient in recipientsToRemove)
                    list.Value.Remove(recipient);

                if (list.Value.Count == 0)
                    listsToRemove.Add(list.Key);
            }

            foreach (var key in listsToRemove)
                lists.Remove(key);
        }

        /// <summary>
        /// Check if the messagetype implements the type.
        /// </summary>
        /// <param name="messageType">Type of the message.</param>
        /// <param name="type">The type to check.</param>
        /// <returns>true if the messagetype implements the type</returns>
        private static bool Implements(Type messageType, Type type)
        {
            if (type == null || messageType == null)
                return false;

            var interfaces = messageType.GetInterfaces();
            foreach (var currentInterface in interfaces)
                if (currentInterface == type)
                    return true;

            return false;
        }

        /// <summary>
        /// Sends to list.
        /// </summary>
        /// <typeparam name="TMessage">The type of the message.</typeparam>
        /// <param name="message">The message.</param>
        /// <param name="list">The list or WeakActions.</param>
        /// <param name="messageTargetType">Type of the message target.</param>
        private static void SendToList<TMessage>(TMessage message, IEnumerable<WeakAction> list, Type messageTargetType)
        {
            if (list != null)
            {
                // Clone to protect from people registering in a "receive message" method
                // Bug correction Messaging BL0004.007
                var listClone = list.Take(list.Count()).ToList();

                foreach (var action in listClone)
                {
                    var executeAction = action as IExecuteWithObject;
                    if (action != null && executeAction != null && action.IsAlive && action.Target != null &&
                        (messageTargetType == null || action.Target.GetType() == messageTargetType))
                        executeAction.ExecuteWithObject(message);
                }
            }
        }

        /// <summary>
        /// Unregisters from lists.
        /// </summary>
        /// <param name="recipient">The recipient.</param>
        /// <param name="lists">The lists.</param>
        private static void UnregisterFromLists(object recipient, Dictionary<Type, List<WeakAction>> lists)
        {
            if (recipient == null || lists == null || lists.Count == 0)
                return;

            lock (lists)
            {
                foreach (var messageType in lists.Keys)
                {
                    foreach (var item in lists[messageType])
                    {
                        var weakAction = item.Action;
                        if (weakAction != null && recipient == weakAction.Target)
                        {
                            item.MarkForDeletion();
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Unregisters from lists.
        /// </summary>
        /// <typeparam name="TMessage">The type of the message.</typeparam>
        /// <param name="recipient">The recipient.</param>
        /// <param name="action">The action.</param>
        /// <param name="lists">The lists.</param>
        private static void UnregisterFromLists<TMessage>(object recipient, Action<TMessage> action, Dictionary<Type, List<WeakAction>> lists)
        {
            var messageType = typeof(TMessage);

            if (recipient == null || lists == null || lists.Count == 0 || !lists.ContainsKey(messageType))
                return;

            lock (lists)
            {
                foreach (var weakAction in lists[messageType])
                {
                    var weakActionCasted = weakAction as WeakAction<TMessage>;
                    if (weakActionCasted != null && recipient == weakActionCasted.Target
                        && (action == null || action == weakActionCasted.Action))
                        weakAction.MarkForDeletion();
                }
            }
        }

        /// <summary>
        /// Cleanups this instance.
        /// </summary>
        private void Cleanup()
        {
            CleanupList(this.recipientsOfSubclassesAction);
            CleanupList(this.recipientsStrictAction);
        }

        /// <summary>
        /// Sends the type of to target or.
        /// </summary>
        /// <typeparam name="TMessage">The type of the message.</typeparam>
        /// <param name="message">The message.</param>
        /// <param name="messageTargetType">Type of the message target.</param>
        private void SendToTargetOrType<TMessage>(TMessage message, Type messageTargetType)
        {
            var messageType = typeof(TMessage);

            if (this.recipientsOfSubclassesAction != null)
            {
                foreach (var type in this.recipientsOfSubclassesAction.Keys)
                {
                    List<WeakAction> list = null;
                    if (messageType == type || messageType.IsSubclassOf(type) || Implements(messageType, type))
                        list = this.recipientsOfSubclassesAction[type];
                    SendToList(message, list, messageTargetType);
                }
            }

            if (this.recipientsStrictAction != null)
            {
                if (this.recipientsStrictAction.ContainsKey(messageType))
                {
                    var list = this.recipientsStrictAction[messageType];
                    SendToList(message, list, messageTargetType);
                }
            }

            this.Cleanup();
        }
    }
}
